package explorandum.f09.g5;

import java.awt.Point;
import java.awt.Rectangle;
import java.util.Set;
import java.util.ArrayList;

import explorandum.GameConstants;

public abstract class Map {
  protected int time;
  protected int num_rounds;
  
  private static final Boolean COAST_MULTIPLIER = true;

  // Add what the player sees to the map.
  public abstract void add(int explorerId, Point currentLocation, Point[] offsets,
                  Boolean[] hasExplorer,
                  Integer[][] otherExplorers, Integer[] terrain, int time, int range, Boolean StepStatus);
  
  public abstract Point getmin();
  
  public abstract Point getmax();
  
  // Get the current bound of the map.
  public abstract Rectangle getBound();

  // Get the cell at (x, y). The coordinates are relative to the start point.
  public abstract Cell getCell(int x, int y);

  // Get the cell at p. The coordinates are relative to the start point.
  public abstract Cell getCell(Point p);

  // Get the coordinates of all discovered cells
  public abstract Set<Point> getAllDiscoveredCoordinates();

  // Print debug message to stdout
  public abstract void printDebugMsg();

  // Get a list of neighbors in some range. This function does consider
  // mountains as occluders.
  public ArrayList<Point> getNeighbors(Point p, double range) {
    ArrayList<Point> neighbors = new ArrayList<Point>();
    for (int x = (int) (p.x - range); x <= p.x + range; x++) {
      for (int y = (int) (p.y - range); y <= p.y + range; y++) {
        Point neighbor_point = new Point(x, y);
        if (p.distance(neighbor_point) <= range + 0.00001) {
          // check if there is a mountain in between
          boolean is_visible = true;
          double distance = p.distance(neighbor_point);
          ArrayList<Point> in_between_cells = Util.getCellsBetween(p, neighbor_point);
          for (Point p_in_between : in_between_cells) {
            if (p_in_between.equals(neighbor_point))
              continue;
            Cell cell_in_between = getCell(p_in_between);
            if (cell_in_between != null && cell_in_between.terrain == 2) {
              is_visible = false;
              break;
            }
          }
          if (is_visible)
            neighbors.add(neighbor_point);
        }
      }
    }
    return neighbors;
  }
  
  /* New Fall 2009 Code
   * 
   * Because I'm too lazy to use the getNeighbors function and get a cell for each,
   *   I wrote a "special case" which grabs the 8 closest Cells, regardless of whether
   *   they can be seen or whether they exist.
   */
  public ArrayList<Cell> getImmediateNeighbors(Point p) {
	  ArrayList<Cell> neighbors = new ArrayList<Cell>();
	  
	  for(int dir = Util.MIN_DIRECTION; dir <= Util.MAX_DIRECTION; dir++) {
		  Cell neighbor = this.getCell(p.x + GameConstants._dx[dir], p.y + GameConstants._dy[dir]);  
		  neighbors.add(neighbor);
	  }
	  
	  return neighbors;
  }

  // Measure the openness of a cell.
  public double getCellOpenness(Point p, double range) {
    Cell c = getCell(p);
    
    Boolean coast = false;
    Boolean foothills = false;
    if (c == null || c.terrain != GameConstants.LAND) {
      return 0;
    }
    
    if (c != null && (c.claimed_distance_other == 0 ||
                      c.claimed_distance == 0)) {
      return 0;
    }

    double openness = 0;
    ArrayList<Point> neighbors = getNeighbors(p, range);
    for (Point neighbor_point : neighbors) {
      c = getCell(neighbor_point);
      if (c != null) {
    	  double all_claimed_distance =
    		  Math.min(c.claimed_distance, c.claimed_distance_other);

    	  /*
    	   * New Fall 2009 Code
    	   * 
    	   * More strictly follow the paradigm of applying a "Multiplier" to a raw openness score,
    	   *   which is "Math.max(0, all_claimed_distance - p.distance(neighbor_point))"
    	   *   
    	   * This way, we can easily add extra bonuses.
    	   */
    	  if(COAST_MULTIPLIER) {
    		  double multiplier = 1;

    		  switch(c.terrain) {
    		  case GameConstants.LAND:
    			  multiplier = 1;
    			  break;
    		  case GameConstants.MOUNTAIN:
    			  multiplier = 1.5;
    			  if(p.distance(neighbor_point) < 1.5) {
    				  //System.out.print("\t\t F [x=" + neighbor_point.x + ",y=" + neighbor_point.y + "] dist == " + p.distance(neighbor_point));
    				  foothills = true;
    			  }
    			  break;
    		  case GameConstants.WATER:
    			  multiplier = getWaterWeight();
    			  if(p.distance(neighbor_point) < 1.5) {
    				  /* current square can be considered a coastline, maybe extra bonus? */
    				  //System.out.print("\t\t C [x=" + neighbor_point.x + ",y=" + neighbor_point.y + "] dist == " + p.distance(neighbor_point));
    				  coast = true;
    			  }
    			  break;
    		  default:
    			  multiplier = 1;
    		  }

    		  openness += Math.max(0, all_claimed_distance - p.distance(neighbor_point)) * multiplier;
    	  }
    	  else {
    		  /* Older code */
    		  if (c.terrain == GameConstants.LAND || c.terrain == GameConstants.MOUNTAIN)
    			  openness += Math.max(0,
    					  all_claimed_distance - p.distance(neighbor_point));
    		  else {  // water
    			  openness += Math.max(0,
    					  (all_claimed_distance - p.distance(neighbor_point)) *
    					  getWaterWeight());
    		  }
    	  }

      }
      else {
    	  openness += (range - p.distance(neighbor_point)) * 2;
      }
    }
    
    if(COAST_MULTIPLIER) {
    	if(coast) {
    		openness *= 1.2;
    		//System.out.println("\t\t" + p + " is a coast");
    	}
    	else if (foothills) {
    		openness *= 1.1;
//    		System.out.println("\t\t" + p + " is foothills");
    	}
    }
    return openness;
  }
  
  protected double getWaterWeight() {
    // A naive weighting function based on time.
    if (time < ((double) num_rounds) * 0.333) {
      return 30;
    }
    return 13 * ((double) num_rounds) / time - 9;
  }

  public Point getNeighborPoint(Point p, int direction) {
    return new Point(
      p.x + GameConstants._dx[direction],
      p.y + GameConstants._dy[direction]);
  }

  public static int getDirection(Point a, Point b) {
    if (b.x == a.x && b.y <  a.y)
      return 1;
    else if (b.x >  a.x && b.y <  a.y)
      return 2;
    else if (b.x >  a.x && b.y == a.y)
      return 3;
    else if (b.x >  a.x && b.y >  a.y)
      return 4;
    else if (b.x == a.x && b.y >  a.y)
      return 5;
    else if (b.x <  a.x && b.y >  a.y)
      return 6;
    else if (b.x <  a.x && b.y == a.y)
      return 7;
    else if (b.x <  a.x && b.y <  a.y)
      return 8;
    return 0;
  }

  public void setTime(int time) {
    this.time = time;
  }

  public void setNumRound(int num_rounds) {
    this.num_rounds = num_rounds;
  }
}
